// HSS.Threading.Timer.cs
// ----------------------------------------------------------------------------
// Licensed under the MIT license
// http://www.opensource.org/licenses/mit-license.html
// ----------------------------------------------------------------------------
// HighSpeed-Solutions, LLC
// Copyright (c) 2001-2010
// ----------------------------------------------------------------------------
// File:       Timer.cs
// Author:     HSS\gbanta
// Created:    08/12/2010
// Modified:   12/04/2010
// ----------------------------------------------------------------------------
namespace HSS.Threading
{
	#region Using Directives
	using System;
	using System.Collections.Generic;
	using System.Threading;

	#endregion

	#region Timer
	/// <summary>
	/// The Timer object is a Threaded Ticker, and supports complex Schedules such
	/// as once a week between the hours of 8:00 AM and 5:00 PM, or every day at
	/// 6:00 AM, or every other week/month, or on Mondays and Tuesdays 24X7. The
	/// other key feature is the option to recur within the day such as, repeat
	/// every 15 minutes or fire continously, such as an open loop. Even though
	/// there is a forced minimum delay of 550 milliseconds, you can override
	/// the delay by calling the <c>SetDelay</c> method.
	/// </summary>
	public sealed class Timer : IDisposable
	{
		#region Fields

		const int ContinousDelay = 550;

		private Dictionary<string, string> propertyBag;
		private ITimerService service;
		private ManualResetEvent mre;
		private TimerSchedule schedule;
		private Thread ticker;
		private volatile int delay;
		private volatile bool disposed;
		private readonly object lockPropb;
		private readonly object lockStart;
		private string startNotStopErrMsg;
		private bool checkBeginningOfWeek;
		#endregion

		#region Constructor
		/// <summary>
		/// The constructor for the Timer
		/// </summary>
		/// <param name="service">The TimerService that owns this Timer</param>
		/// <param name="schedule">The Schedule used for the Timer</param>
		public Timer(ITimerService service, TimerSchedule schedule)
		{
			this.propertyBag = new Dictionary<string, string>();
			this.service = service;
			this.schedule = schedule;
			this.lockPropb = new object();
			this.lockStart = new object();

			this.startNotStopErrMsg = "Cannot submit a start request when not in a stopped or paused state.";
		}
		/// <summary>
		/// Clean up external resources
		/// </summary>
		public void Dispose()
		{
			this.Close();
			GC.SuppressFinalize(this);
		}
		/// <summary>
		/// Release Unmanaged Resources
		/// </summary>
		/// <param name="Disposing">Are we closing</param>
		private void Dispose(bool Disposing)
		{
			// If we're closed simply return
			if (this.disposed)
				return;

			if (Disposing)
			{
				try { this._timerStatus = TimerState.Stopped; }
				catch { }

				try { this.OnStop(); }
				catch { }

				try
				{
					this.propertyBag.Clear();
					if (null != mre)
						this.mre.Close();
				}
				catch { }
			}
			this.disposed = true;
		}
		#endregion

		#region PropertyBag Methods
		private void SetProperty(string Name, string Value)
		{
			lock (lockPropb)
			{
				if (propertyBag.ContainsKey(Name))
					propertyBag[Name] = Value;
				else
					propertyBag.Add(Name, Value);
			}
		}
		private string GetProperty(string Name, string DefaultValue)
		{
			string val;
			lock (lockPropb)
			{
				if (!propertyBag.TryGetValue(Name, out val))
				{
					propertyBag.Add(Name, DefaultValue);
					val = DefaultValue;
				}
			}
			return val;
		}
		#endregion

		#region Control Methods
		/// <summary>
		/// Start the timer
		/// </summary>
		/// <exception cref="InvalidOperationException">Cannot submit a start request when not in a stopped or paused state.</exception>
		/// <exception cref="ObjectDisposedException">The instance of the Timer is disposed</exception>
		public void Start()
		{
			// Only if stopped or paused
			if (this._timerStatus != TimerState.Stopped
				&& this._timerStatus != TimerState.Paused)
				throw new InvalidOperationException(startNotStopErrMsg);

			// Not if disposed
			if (this.disposed)
				throw new ObjectDisposedException("HSS.Threading.Timer");

			// Set Status
			this._timerStatus = TimerState.Starting;

			// Initial Delay
			this.delay = Timer.ContinousDelay;

			lock (this.lockStart)
			{
				// Delay wait event
				this.mre = new ManualResetEvent(false);

				// Create ticker Thread
				ticker = new Thread(this.OnStart);
				ticker.IsBackground = true;

				// Start ticker
				ticker.Start();

				// Set status
				this._timerStatus = TimerState.Started;

				// Wait for the Thread to Start
				Monitor.Wait(this.lockStart);
			}
		}
		/// <summary>
		/// Stop the Timer
		/// </summary>
		public void Stop()
		{
			// If we're disposed simply return
			if (this.disposed)
				return;

			// Only if we're Running
			if (this._timerStatus == TimerState.Started)
			{
				// Change Status
				this._timerStatus = TimerState.Stopping;

				// Stop Ticker
				this.OnStop();
			}

			// Change Status
			if (this._timerStatus != TimerState.Stopped)
				this._timerStatus = TimerState.Stopped;

		}
		/// <summary>
		/// Pause the Timer
		/// </summary>
		public void Pause()
		{
			// If we're disposed simply return
			if (this.disposed)
				return;

			// Only if we're Running
			if (this._timerStatus == TimerState.Started)
			{
				// Change Status
				this._timerStatus = TimerState.Pausing;

				// Stop Ticker
				this.OnStop();

				// Change Status
				this._timerStatus = TimerState.Paused;
			}
		}
		/// <summary>
		/// Continue the timer
		/// </summary>
		public void Continue()
		{
			// If we're disposed simply return
			if (this.disposed)
				return;

			// If we're paused, simply start
			if (this._timerStatus == TimerState.Paused)
				this.Start();
		}
		/// <summary>
		/// Close the Timer
		/// </summary>
		public void Close()
		{
			this.Dispose(true);
		}
		/// <summary>
		/// Allows the delay value to be manually set thereby overriding the delay until the next tick. If the current 
		/// Frequency is not set to Continuous, then this method is ignored and no action is taken
		/// </summary>
		/// <remarks>
		/// This is to allow for a manual (programmatic) override.
		/// 
		/// NOTE: The delay value will auto reset to continuous (550ms) at next tick and you will have to 
		/// re-evaluate if you need to call SetDelay again
		/// </remarks>
		/// <param name="delay">Delay in Seconds</param>
		public void SetDelay(int delay)
		{
			if (schedule.IntervalFrequency == ScheduleIntervalFrequency.Continuous)
				this.delay = delay * 1000;
		}

		private void OnStart()
		{
			lock (this.lockStart)
			{
				// Let the Start method know we've started
				Monitor.Pulse(this.lockStart);
			}

			while (this._timerStatus == TimerState.Started)
			{
				// Check timer
				if (this.Evaluate())
					this.service.OnTick(this);

				// If we've stopped, bail out of the loop
				if (this._timerStatus != TimerState.Started)
					return;

				// Wait the Delay, then re-evaluate...
				if (mre.WaitOne(this.delay, false))
					return;
			}
		}
		private void OnStop()
		{
			// Stop Ticker
			if (null != this.ticker)
			{
				if (this.ticker.IsAlive)
				{
					this.mre.Set();
					this.ticker.Join(1500);
				}
				this.ticker = null;
			}
		}

		#endregion

		#region Evaluation Methods

		private bool Evaluate()
		{
			bool okToStart = false;

			switch (this.schedule.Frequency)
			{
				case ScheduleFrequency.Daily:
					okToStart = EvaluateDaily();
					break;

				case ScheduleFrequency.Weekly:
					okToStart = EvaluateWeekly();
					break;

				case ScheduleFrequency.Monthly:
					okToStart = EvaluateMonthly();
					break;
			}

			return okToStart;
		}
		private bool EvaluateExclusions(out ScheduleExclusion exclusions)
		{
			exclusions = null;
			// Compare Now to ScheduleExclusion
			bool okToRun = true;
			DateTime now = DateTime.Now;
			int yr = now.Year;
			int mo = now.Month;
			int dy = now.Day;
			DateTime day;
			DateTime start;
			DateTime end;
			bool allDay;

			foreach (ScheduleExclusion exclusion in this.schedule.Exclusions)
			{
				day = exclusion.Day;
				start = exclusion.StartTime;
				end = exclusion.EndTime;
				allDay = exclusion.AllDay;

				if (day.Year == yr && day.Month == mo && day.Day == dy)
				{
					if (allDay) okToRun = false;
					else okToRun = !(now.TimeOfDay >= start.TimeOfDay && now.TimeOfDay <= end.TimeOfDay);

					// If we can't run, go ahead and break as we don't need to continue checking
					if (!okToRun)
					{
						exclusions = exclusion;
						return okToRun;
					}
				}
			}

			return okToRun;

		}
		private bool EvaluateExclusions()
		{
			// Compare Now to ScheduleExclusion
			bool okToRun = true;
			DateTime now = DateTime.Now;
			int yr = now.Year;
			int mo = now.Month;
			int dy = now.Day;
			DateTime day;
			DateTime start;
			DateTime end;
			bool allDay;

			foreach (ScheduleExclusion exclusion in this.schedule.Exclusions)
			{
				day = exclusion.Day;
				start = exclusion.StartTime;
				end = exclusion.EndTime;
				allDay = exclusion.AllDay;

				if (day.Year == yr && day.Month == mo && day.Day == dy)
				{
					if (allDay) okToRun = false;
					else okToRun = !(now.TimeOfDay >= start.TimeOfDay && now.TimeOfDay <= end.TimeOfDay);

					// If we can't run, go ahead and break as we don't need to continue checking
					if (!okToRun)
						return okToRun;
				}
			}

			return okToRun;

		}

		private bool EvaluateDaily()
		{
			return EvaluateTimeToRun(Daily_CanRunToday());
		}
		private bool EvaluateWeekly()
		{
			return EvaluateTimeToRun(Weekly_CanRunToday());
		}
		private bool EvaluateMonthly()
		{
			return EvaluateTimeToRun(Monthly_CanRunToday());
		}

		private bool EvaluateTimeToRun(bool Run)
		{
			if (this.schedule.DailyType == ScheduleDailyType.OnceADay)
				return EvaluateOnceADay(Run);

			if (this.schedule.DailyType == ScheduleDailyType.Repeatedly)
				return EvaluateInterval(Run);

			return false;
		}
		private bool EvaluateOnceADay(bool Run)
		{
			if (Run)
			{
				if (this.schedule.OnceAt.Hour == DateTime.Now.Hour &&
					this.schedule.OnceAt.Minute == DateTime.Now.Minute)
				{
					// OK to Run as long as it's not a exclusion; come back at Midnight either way
					this.SetDelayToMidnight();
					return this.EvaluateExclusions();
				}
				else
				{
					// NOT OK to Run, check back at Scheduled Time
					this.CalculateDelay_OnceADay();
					return false;
				}
			}
			else
			{
				// Just come back at Midnight
				this.SetDelayToMidnight();
				return false;
			}
		}
		private bool EvaluateInterval(bool Run)
		{
			bool okToStart = true;

			if (Run)
			{
				#region Determine if we're within the Allowable Running Time
				if (IsScheduledPastMidnight())
				{
					if (DateTime.Now.TimeOfDay > this.schedule.IntervalEnds.TimeOfDay &&
						DateTime.Now.TimeOfDay < this.schedule.IntervalBegins.TimeOfDay)
					{
						// Outside the range of available run time
						okToStart = false;
					}
					else
					{
						// Inside the range of available run time
						okToStart = true;
					}
				}
				else
				{
					if (DateTime.Now.TimeOfDay >= this.schedule.IntervalBegins.TimeOfDay &&
						DateTime.Now.TimeOfDay <= this.schedule.IntervalEnds.TimeOfDay)
					{
						// Inside the range of available run time
						okToStart = true;
					}
					else
					{
						// Outside the range of available run time
						okToStart = false;
					}
				}
				#endregion

				ScheduleExclusion exclusion;
				if (this.EvaluateExclusions(out exclusion))
				{
					CalculateDelay_Interval(okToStart);
				}
				else
				{
					okToStart = false;

					#region Set delay based on exclusion
					if (exclusion.AllDay)
					{
						// Just come back at Midnight
						SetDelayToMidnight();
					}
					else
					{
						// Comeback after exclusion expires
						DateTime dt =
							new DateTime(DateTime.Now.Year
							, DateTime.Now.Month
							, DateTime.Now.Day
							, exclusion.EndTime.Hour
							, exclusion.EndTime.Minute
							, 1
							, 1);
						TimeSpan tsDelay = dt.Subtract(DateTime.Now);
						this.delay = Convert.ToInt32(tsDelay.TotalMilliseconds);
					}
					#endregion
				}
			}
			else
			{
				// Come back at Midnight
				SetDelayToMidnight();
			}

			return okToStart;
		}

		#endregion

		#region Helper Methods
		private void CalculateDelay_OnceADay()
		{
			int timerHour = this.schedule.OnceAt.Hour;
			int timerMinute = this.schedule.OnceAt.Minute;

			// If we're in the same hour as when we need to
			// run, then evaluate the minute
			if (DateTime.Now.Hour == timerHour)
			{
				if (DateTime.Now.Minute == timerMinute)
				{
					#region Need to run NOW!
					this.delay = 2;
					return;
					#endregion
				}
				else if (DateTime.Now.Minute < timerMinute)
				{
					#region Delay

					DateTime dt =
						new DateTime(DateTime.Now.Year
						, DateTime.Now.Month
						, DateTime.Now.Day
						, timerHour
						, timerMinute
						, 1
						, 1);
					TimeSpan tsDelay = dt.Subtract(DateTime.Now);
					this.delay = Convert.ToInt32(tsDelay.TotalMilliseconds);
					return;

					#endregion
				}
				else
				{
					// Past the time to run, don't re-evaluate until just before midnight
					SetDelayToMidnight();
				}
			}
			// If the current hour is less then the
			// hour to run, then delay until time to run
			else if (DateTime.Now.Hour < timerHour)
			{
				#region Delay

				DateTime dt =
					new DateTime(DateTime.Now.Year
					, DateTime.Now.Month
					, DateTime.Now.Day
					, timerHour
					, timerMinute
					, 1
					, 1);
				TimeSpan tsDelay = dt.Subtract(DateTime.Now);
				this.delay = Convert.ToInt32(tsDelay.TotalMilliseconds);
				return;

				#endregion
			}
			else
			{
				// Past the time to run, don't re-evaluate until just before midnight
				SetDelayToMidnight();
			}
		}
		private void CalculateDelay_Interval(bool Run)
		{
			if (Run)
			{
				#region Set delay equal to the Interval Specified
				switch (this.schedule.IntervalFrequency)
				{
					case ScheduleIntervalFrequency.Continuous:
						this.delay = Timer.ContinousDelay;
						break;
					case ScheduleIntervalFrequency.Seconds:
						this.delay = (this.schedule.Interval * 1000);
						break;
					case ScheduleIntervalFrequency.Minutes:
						this.delay = (this.schedule.Interval * 60000);
						break;
					case ScheduleIntervalFrequency.Hours:
						this.delay = (this.schedule.Interval * 3600000);
						break;
				}
				#endregion
			}
			else
			{
				#region Delay until our next Start Time
				// If not with in the range of time to execute, then 
				// delay until our next start time
				int startHour = this.schedule.IntervalBegins.Hour;
				int startMinute = this.schedule.IntervalBegins.Minute;

				if (DateTime.Now.Hour == startHour)
				{
					#region Same Hour
					// If we're in the same hour as when we can
					// run, then evaluate the minute
					if (DateTime.Now.Minute == startMinute)
					{
						#region Same Minute
						// If we're in the same minute then
						// it's time to start running
						this.delay = 850;
						return;
						#endregion
					}
					else if (DateTime.Now.Minute < startMinute)
					{
						#region Before our Start Time
						// We're in the same hour as we're supposed to run
						// but still not quite time yet so delay
						DateTime dt =
							new DateTime(DateTime.Now.Year
							, DateTime.Now.Month
							, DateTime.Now.Day
							, startHour
							, startMinute
							, 1
							, 1);
						TimeSpan tsDelay = dt.Subtract(DateTime.Now);
						this.delay = Convert.ToInt32(tsDelay.TotalMilliseconds);
						return;

						#endregion
					}
					else // DateTime.Now.Minute > startMinute
					{
						#region After our Start Time
						// Our start time is in the next day, so delay until Midnight
						SetDelayToMidnight();
						return;

						#endregion
					}
					#endregion
				}
				else if (DateTime.Now.Hour < startHour)
				{
					#region Before our Start Time
					// If the current hour is less then the hour to 
					// run, then delay until our Start Time
					DateTime dt =
						new DateTime(DateTime.Now.Year
						, DateTime.Now.Month
						, DateTime.Now.Day
						, startHour
						, startMinute
						, 1
						, 1);
					TimeSpan tsDelay = dt.Subtract(DateTime.Now);
					this.delay = Convert.ToInt32(tsDelay.TotalMilliseconds);
					return;

					#endregion
				}
				else // DateTime.Now.Hour > startHour
				{
					#region After our Start Time
					// Our start time is in the next day, so delay until Midnight
					SetDelayToMidnight();
					return;
					#endregion
				}
				#endregion
			}
		}

		private bool Daily_CanRunToday()
		{
			// Every X Number of Days (1-31)
			int day = int.Parse(this.GetProperty("Daily_CurrentDay", "0"));
			if (day == 0)
			{
				// If we've hit Zero, then it's the day we're to run, reset to Occurrence
				this.SetProperty("Daily_CurrentDay", this.schedule.Occurrence.ToString());
				day = this.schedule.Occurrence;
			}

			// Evaluate moving to the next day
			string weekDay = this.GetProperty("Daily_DayOfWeek", DateTime.Today.DayOfWeek.ToString());
			if (DateTime.Today.DayOfWeek.ToString() != weekDay)
			{
				// We moved into the Next Day, Subtract one from 'day'
				if (day > 0)
					day--;

				// If we've hit Zero, then it's the day we're to run, reset to Occurrence
				if (day == 0)
				{
					this.SetProperty("Daily_CurrentDay", this.schedule.Occurrence.ToString());
					day = this.schedule.Occurrence;
				}
				else
				{
					this.SetProperty("Daily_CurrentDay", day.ToString());
				}

				// And update the Day of the Week
				this.SetProperty("Daily_DayOfWeek", DateTime.Today.DayOfWeek.ToString());
			}

			// If day == Occurrence then OK to run today
			return (day == this.schedule.Occurrence);

		}

		private bool Weekly_CanRunToday()
		{
			bool OkRunWeek = bool.TrueString == this.GetProperty("Weekly_RunThisWeek", bool.TrueString);

			// If it's Monday determine if we can run this Week
			if (DateTime.Today.DayOfWeek == DayOfWeek.Monday)
			{
				// If the Check Flag is ON Evaluate Week
				if (this.checkBeginningOfWeek == true)
				{
					// Turn OFF beginning of week check
					this.checkBeginningOfWeek = false;

					//Re-Evaluate if this week is ok to run
					if (!Weekly_CanRunThisWeek())
						return false;
					else
						OkRunWeek = true;
				}
			}

			if (DateTime.Today.DayOfWeek == DayOfWeek.Tuesday)
			{
				// Turn ON Beginning of Week Check
				this.checkBeginningOfWeek = true;
			}

			// Ok To Run this Week 
			if (OkRunWeek)
			{
				switch (DateTime.Today.DayOfWeek)
				{
					case DayOfWeek.Monday:
						if ((this.schedule.OccurrenceFlag & TimerSchedule.Monday) != 0)
							return true;
						break;

					case DayOfWeek.Tuesday:
						if ((this.schedule.OccurrenceFlag & TimerSchedule.Tuesday) != 0)
							return true;
						break;

					case DayOfWeek.Wednesday:
						if ((this.schedule.OccurrenceFlag & TimerSchedule.Wednesday) != 0)
							return true;
						break;

					case DayOfWeek.Thursday:
						if ((this.schedule.OccurrenceFlag & TimerSchedule.Thursday) != 0)
							return true;
						break;

					case DayOfWeek.Friday:
						if ((this.schedule.OccurrenceFlag & TimerSchedule.Friday) != 0)
							return true;
						break;

					case DayOfWeek.Saturday:
						if ((this.schedule.OccurrenceFlag & TimerSchedule.Saturday) != 0)
							return true;
						break;

					case DayOfWeek.Sunday:
						if ((this.schedule.OccurrenceFlag & TimerSchedule.Sunday) != 0)
							return true;
						break;

					default:
						return false;
				}
			}

			return false;

		}
		private bool Weekly_CanRunThisWeek()
		{
			int WeekCounter = int.Parse(this.GetProperty("Weekly_WeekCounter", "1"));
			bool run = false;

			// Increase Week Count
			WeekCounter++;

			// Should we skip this week?
			if (WeekCounter > 1 && this.schedule.Occurrence > 1)
			{
				run = false;

				if (WeekCounter == this.schedule.Occurrence)
					// Reset
					this.SetProperty("Weekly_WeekCounter", "1");
				else
					// Record next iteration
					this.SetProperty("Weekly_WeekCounter", WeekCounter.ToString());
			}
			else
			{
				run = true;
			}

			if (run)
				this.SetProperty("Weekly_RunThisWeek", bool.TrueString);
			else
				this.SetProperty("Weekly_RunThisWeek", bool.FalseString);

			return run;
		}

		private bool Monthly_CanRunToday()
		{
			// If this is the Day, is it the correct Month
			if (this.schedule.OccurrenceFlag == DateTime.Today.Day)
			{
				return Monthly_CanRunThisMonth();
			}
			return false;
		}
		private bool Monthly_CanRunThisMonth()
		{
			// Every X Number of Months (1-12)
			int month = int.Parse(this.GetProperty("Monthly_CurrentMonth", "0"));
			if (month == 0)
			{
				// If we've hit Zero, then it's the month we're to run, reset to Occurrence
				this.SetProperty("Monthly_CurrentMonth", this.schedule.Occurrence.ToString());
				month = this.schedule.Occurrence;
			}

			// Evaluate moving to the next month
			string yearlyMonth = this.GetProperty("Monthly_MonthOfYear", DateTime.Today.Month.ToString());
			if (DateTime.Today.Month.ToString()
				!= yearlyMonth)
			{
				// We moved into the Next Month, Subtract one from 'month'
				month--;

				// If we've hit Zero, then it's the month we're to run, reset to Occurrence
				if (month == 0)
				{
					this.SetProperty("Monthly_CurrentMonth", this.schedule.Occurrence.ToString());
					month = this.schedule.Occurrence;
				}
				else
				{
					this.SetProperty("Monthly_CurrentMonth", month.ToString());
				}

				// And update the Month of the Year
				this.SetProperty("Monthly_MonthOfYear", DateTime.Today.Month.ToString());
			}

			// If month == Occurrence then OK to run this Month
			if (month == this.schedule.Occurrence)
			{
				return true;
			}

			return false;
		}


		private int GetMillisecondsToMidnight()
		{
			int nowHour = DateTime.Now.Hour;
			int diffHour = 23 - nowHour;

			int nowMin = DateTime.Now.Minute;
			int diffMin = 59 - nowMin;

			int nowSec = DateTime.Now.Second;
			int diffSec = 59 - nowSec;

			int nowMilli = DateTime.Now.Millisecond;
			int diffMilli = 1000 - nowMilli;

			TimeSpan ts = new TimeSpan(0, diffHour, diffMin, diffSec, diffMilli);

			return (int)ts.TotalMilliseconds;
		}
		private void SetDelayToMidnight()
		{
			this.delay = GetMillisecondsToMidnight();

			if (this.delay > 240000)
			{
				// If we have at least 4 minutes until midnight, subtract 2 minutes
				// from the amount of time till midnight so we can re-evaluate 2 minutes 
				// before midnight.
				// This is so we can shorten the delay, and have a more precise measurement of
				// when we are to run, ensuring that we don't miss the minute before or the minute
				// after midnight.
				this.delay = this.delay - 120000;
			}
			else
			{
				// Otherwise, it's less then four minutes to midnight
				// So set the interval to less than a minute. Again this is to
				// ensure we don't miss the minute before or the minute after 
				// midnight.
				this.delay = 25000; // 25 seconds, should guarantee we hit the minute
			}
		}
		private bool IsScheduledPastMidnight()
		{
			return
				this.schedule.IntervalBegins.TimeOfDay >=
				this.schedule.IntervalEnds.TimeOfDay;
		}
		#endregion

		#region Properties
		/// <summary>
		/// Gets or sets the Timer State
		/// </summary>
		public TimerState State
		{
			get { return _timerStatus; }
			set { _timerStatus = value; }
		} volatile TimerState _timerStatus = TimerState.Stopped;
		#endregion
	}
	#endregion
}